Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit e3a56d5ecea2a6bd62668e2fc052c0ebfab55961


Parents : 4657613
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-09T12:32:36-05:00

feat(error-reporting): update startup error handling with detailed diagnostics and refactor error message formatting

Changes
Diff

diff --git a/android/app/src/main/java/com/meshchatx/MainActivity.java b/android/app/src/main/java/com/meshchatx/MainActivity.java
index ddc4bdb6..378eb8ab 100644
--- a/android/app/src/main/java/com/meshchatx/MainActivity.java
+++ b/android/app/src/main/java/com/meshchatx/MainActivity.java
@@ -267,7 +267,7 @@ public class MainActivity extends AppCompatActivity {
view.loadUrl("about:blank");
if (backendFailed && !startupPageLoaded) {
CharSequence description = (error != null) ? error.getDescription() : "Unknown error";
- showStartupError("WebView failed to load MeshChatX: " + description);
+ showStartupError("WebView failed to load MeshChatX:", description);
}
}
}
@@ -280,7 +280,7 @@ public class MainActivity extends AppCompatActivity {
view.stopLoading();
view.loadUrl("about:blank");
if (backendFailed) {
- showStartupError("WebView failed to load MeshChatX: " + description);
+ showStartupError("WebView failed to load MeshChatX:", description);
}
}
}
@@ -459,7 +459,8 @@ public class MainActivity extends AppCompatActivity {
AndroidStorageManager.runPendingMigration(this);
} catch (IOException migrationError) {
showStartupError(
- "MeshChatX could not copy data to file-manager storage:\n" + migrationError.getMessage()
+ "MeshChatX could not copy data to file-manager storage:",
+ migrationError.getMessage()
);
return;
}
@@ -661,7 +662,7 @@ public class MainActivity extends AppCompatActivity {
mainHandler.postDelayed(() -> startMeshChatServer(), MESHCHAT_SERVER_RETRY_DELAY_MS);
} else {
backendFailed = true;
- showStartupError("MeshChatX backend failed:\n" + stack);
+ showStartupError("MeshChatX backend failed:", stack);
}
});
}
@@ -767,7 +768,10 @@ public class MainActivity extends AppCompatActivity {
}
connectionAttempts += 1;
if (connectionAttempts > MAX_CONNECTION_ATTEMPTS) {
- showStartupError("Failed to connect to local MeshChatX server after waiting for startup.");
+ showStartupError(
+ "Failed to connect to local MeshChatX server after waiting for startup.",
+ null
+ );
return;
}
webView.loadUrl(SERVER_URL);
@@ -801,13 +805,26 @@ public class MainActivity extends AppCompatActivity {
}
}
- private void showStartupError(String message) {
+ private void showStartupError(String title, @Nullable String detail) {
+ final String message = StartupErrorReport.format(
+ title,
+ detail,
+ StartupErrorReport.collect(this)
+ );
runOnUiThread(() -> {
mainHandler.removeCallbacksAndMessages(null);
- webView.setVisibility(android.view.View.INVISIBLE);
- loadingLogo.setVisibility(android.view.View.GONE);
- progressBar.setVisibility(android.view.View.GONE);
- loadingText.setVisibility(android.view.View.GONE);
+ if (webView != null) {
+ webView.setVisibility(android.view.View.INVISIBLE);
+ }
+ if (loadingLogo != null) {
+ loadingLogo.setVisibility(android.view.View.GONE);
+ }
+ if (progressBar != null) {
+ progressBar.setVisibility(android.view.View.GONE);
+ }
+ if (loadingText != null) {
+ loadingText.setVisibility(android.view.View.GONE);
+ }
if (errorText != null) {
errorText.setText(message);
errorText.setVisibility(android.view.View.VISIBLE);

diff --git a/android/app/src/main/java/com/meshchatx/StartupErrorReport.java b/android/app/src/main/java/com/meshchatx/StartupErrorReport.java
new file mode 100644
index 00000000..905bd59a
--- /dev/null
+++ b/android/app/src/main/java/com/meshchatx/StartupErrorReport.java
@@ -0,0 +1,138 @@
+package com.meshchatx;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Formats Android startup / backend failure messages with app and device diagnostics
+ * so crash reports from older builds are easier to triage.
+ */
+public final class StartupErrorReport {
+
+ private StartupErrorReport() {}
+
+ public static final class Diagnostics {
+ public final String appVersionName;
+ public final String appVersionCode;
+ public final String androidRelease;
+ public final int sdkInt;
+ public final String manufacturer;
+ public final String model;
+ public final String device;
+
+ public Diagnostics(
+ @Nullable String appVersionName,
+ @Nullable String appVersionCode,
+ @Nullable String androidRelease,
+ int sdkInt,
+ @Nullable String manufacturer,
+ @Nullable String model,
+ @Nullable String device
+ ) {
+ this.appVersionName = appVersionName;
+ this.appVersionCode = appVersionCode;
+ this.androidRelease = androidRelease;
+ this.sdkInt = sdkInt;
+ this.manufacturer = manufacturer;
+ this.model = model;
+ this.device = device;
+ }
+ }
+
+ @NonNull
+ public static Diagnostics collect(@NonNull Context context) {
+ String versionName = "unknown";
+ String versionCode = "unknown";
+ try {
+ PackageInfo pi = context
+ .getPackageManager()
+ .getPackageInfo(context.getPackageName(), 0);
+ if (pi.versionName != null && !pi.versionName.isEmpty()) {
+ versionName = pi.versionName;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ versionCode = Long.toString(pi.getLongVersionCode());
+ } else {
+ versionCode = Integer.toString(pi.versionCode);
+ }
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ return new Diagnostics(
+ versionName,
+ versionCode,
+ Build.VERSION.RELEASE,
+ Build.VERSION.SDK_INT,
+ Build.MANUFACTURER,
+ Build.MODEL,
+ Build.DEVICE
+ );
+ }
+
+ @NonNull
+ public static String format(
+ @NonNull String title,
+ @Nullable String detail,
+ @NonNull Diagnostics diagnostics
+ ) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(title == null ? "MeshChatX error" : title.trim());
+ sb.append('\n');
+ sb.append(formatDiagnosticsBlock(diagnostics));
+ if (detail != null) {
+ String trimmed = detail.trim();
+ if (!trimmed.isEmpty()) {
+ sb.append("\n\n");
+ sb.append(trimmed);
+ }
+ }
+ return sb.toString();
+ }
+
+ @NonNull
+ public static String formatDiagnosticsBlock(@NonNull Diagnostics diagnostics) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("App: ");
+ sb.append(safe(diagnostics.appVersionName, "unknown"));
+ String code = safe(diagnostics.appVersionCode, "");
+ if (!code.isEmpty() && !"unknown".equals(code)) {
+ sb.append(" (").append(code).append(')');
+ }
+ sb.append('\n');
+ sb.append("Android: ");
+ sb.append(safe(diagnostics.androidRelease, "unknown"));
+ if (diagnostics.sdkInt > 0) {
+ sb.append(" (API ").append(diagnostics.sdkInt).append(')');
+ }
+ sb.append('\n');
+ sb.append("Device: ");
+ String manufacturer = safe(diagnostics.manufacturer, "");
+ String model = safe(diagnostics.model, "");
+ if (!manufacturer.isEmpty() && !model.isEmpty()) {
+ sb.append(manufacturer).append(' ').append(model);
+ } else if (!model.isEmpty()) {
+ sb.append(model);
+ } else if (!manufacturer.isEmpty()) {
+ sb.append(manufacturer);
+ } else {
+ sb.append("unknown");
+ }
+ String device = safe(diagnostics.device, "");
+ if (!device.isEmpty() && !device.equalsIgnoreCase(model)) {
+ sb.append(" [").append(device).append(']');
+ }
+ return sb.toString();
+ }
+
+ @NonNull
+ private static String safe(@Nullable String value, @NonNull String fallback) {
+ if (value == null) {
+ return fallback;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? fallback : trimmed;
+ }
+}

diff --git a/android/app/src/main/python/meshchat_wrapper.py b/android/app/src/main/python/meshchat_wrapper.py
index 268a0b7d..421b2f39 100644
--- a/android/app/src/main/python/meshchat_wrapper.py
+++ b/android/app/src/main/python/meshchat_wrapper.py
@@ -163,7 +163,12 @@ def start_server(port=8000, app_files_dir=None):
# Chaquopy surfaces SystemExit as PyException; re-raise as RuntimeError
# so Java retry/error UI gets a readable message.
code = getattr(e, "code", e)
- message = f"MeshChatX exited during startup (code={code})"
+ cause = e.__cause__ or e.__context__
+ cause_text = str(cause).strip() if cause is not None else ""
+ if cause_text:
+ message = f"MeshChatX exited during startup (code={code}): {cause_text}"
+ else:
+ message = f"MeshChatX exited during startup (code={code})"
print(f"Error starting MeshChatX server: {message}")
raise RuntimeError(message) from e
except Exception as e:

diff --git a/android/app/src/test/java/com/meshchatx/StartupErrorReportTest.java b/android/app/src/test/java/com/meshchatx/StartupErrorReportTest.java
new file mode 100644
index 00000000..8ce271f6
--- /dev/null
+++ b/android/app/src/test/java/com/meshchatx/StartupErrorReportTest.java
@@ -0,0 +1,65 @@
+package com.meshchatx;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class StartupErrorReportTest {
+
+ @Test
+ public void formatIncludesAppAndroidAndDevice() {
+ StartupErrorReport.Diagnostics diagnostics = new StartupErrorReport.Diagnostics(
+ "4.7.2",
+ "472",
+ "13",
+ 33,
+ "Google",
+ "Pixel 6",
+ "oriole"
+ );
+ String report = StartupErrorReport.format(
+ "MeshChatX backend failed:",
+ "com.chaquo.python.PyException: SystemExit: 1\nat meshchat.py:401",
+ diagnostics
+ );
+ Assert.assertTrue(report.startsWith("MeshChatX backend failed:"));
+ Assert.assertTrue(report.contains("App: 4.7.2 (472)"));
+ Assert.assertTrue(report.contains("Android: 13 (API 33)"));
+ Assert.assertTrue(report.contains("Device: Google Pixel 6 [oriole]"));
+ Assert.assertTrue(report.contains("SystemExit: 1"));
+ }
+
+ @Test
+ public void formatDiagnosticsBlockHandlesMissingFields() {
+ StartupErrorReport.Diagnostics diagnostics = new StartupErrorReport.Diagnostics(
+ null,
+ null,
+ null,
+ 0,
+ null,
+ null,
+ null
+ );
+ String block = StartupErrorReport.formatDiagnosticsBlock(diagnostics);
+ Assert.assertTrue(block.contains("App: unknown"));
+ Assert.assertTrue(block.contains("Android: unknown"));
+ Assert.assertTrue(block.contains("Device: unknown"));
+ }
+
+ @Test
+ public void formatOmitsEmptyDetail() {
+ StartupErrorReport.Diagnostics diagnostics = new StartupErrorReport.Diagnostics(
+ "1.0.0",
+ "1",
+ "14",
+ 34,
+ "Samsung",
+ "SM-G991B",
+ "o1s"
+ );
+ String report = StartupErrorReport.format("WebView failed", " ", diagnostics);
+ Assert.assertEquals(
+ "WebView failed\nApp: 1.0.0 (1)\nAndroid: 14 (API 34)\nDevice: Samsung SM-G991B [o1s]",
+ report
+ );
+ }
+}

diff --git a/tests/electron/backendProcessOrphans.test.js b/tests/electron/backendProcessOrphans.test.js
index a1c1307c..aeb3a09f 100644
--- a/tests/electron/backendProcessOrphans.test.js
+++ b/tests/electron/backendProcessOrphans.test.js
@@ -1,20 +1,12 @@
import { describe, expect, it } from "vitest";
-import {
- isHeadlessBackendArgs,
-} from "../../electron/backendProcessOrphans.js";
+import { isHeadlessBackendArgs } from "../../electron/backendProcessOrphans.js";
describe("electron/backendProcessOrphans", () => {
it("detects headless backend command lines", () => {
- expect(
- isHeadlessBackendArgs(
- "/tmp/.mount_x/resources/backend/ReticulumMeshChatX --headless --port 9337"
- )
- ).toBe(true);
- expect(
- isHeadlessBackendArgs(
- "/home/user/ReticulumMeshChatX --headless"
- )
- ).toBe(true);
+ expect(isHeadlessBackendArgs("/tmp/.mount_x/resources/backend/ReticulumMeshChatX --headless --port 9337")).toBe(
+ true
+ );
+ expect(isHeadlessBackendArgs("/home/user/ReticulumMeshChatX --headless")).toBe(true);
});
it("ignores non-backend and non-headless processes", () => {

diff --git a/tests/test_android_meshchat_wrapper.py b/tests/test_android_meshchat_wrapper.py
index 95117bc7..bd18d73b 100644
--- a/tests/test_android_meshchat_wrapper.py
+++ b/tests/test_android_meshchat_wrapper.py
@@ -91,3 +91,25 @@ def test_clear_stale_storage_lock_missing_is_ok(tmp_path):
storage = tmp_path / "storage"
storage.mkdir()
meshchat_wrapper._clear_stale_storage_lock(str(storage))
+
+
+def test_start_server_systemexit_includes_cause(monkeypatch):
+ import pytest
+
+ import meshchatx.meshchat as mm
+
+ def fake_main():
+ try:
+ raise RuntimeError("storage lock held by pid 12345")
+ except RuntimeError as exc:
+ raise SystemExit(1) from exc
+
+ monkeypatch.setattr(mm, "main", fake_main)
+ import meshchat_wrapper
+
+ importlib.reload(meshchat_wrapper)
+ with pytest.raises(RuntimeError) as excinfo:
+ meshchat_wrapper.start_server(8000, None)
+ message = str(excinfo.value)
+ assert "code=1" in message
+ assert "storage lock held by pid 12345" in message


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────